Skip to content

UN-3883 [FEAT] Cut dashboard metrics cron DB load: narrower source windows, monthly from daily, two new indexes - #2276

Merged
kirtimanmishrazipstack merged 34 commits into
mainfrom
UN-3883-Optimize-DB-cron-queries-causing-high-DB-load
Sep 9, 2026
Merged

kirtimanmishrazipstack merged 34 commits into
mainfrom
UN-3883-Optimize-DB-cron-queries-causing-high-DB-load

Conversation

@kirtimanmishrazipstack

@kirtimanmishrazipstack kirtimanmishrazipstack commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

What

The dashboard metrics cron does far less database work. Four changes, already reviewed and merged individually as #2255, #2264 and #2265:

  • Monthly totals are added up from the daily totals instead of being recalculated from the raw tables every time.
  • Each run looks back 2 days instead of ~2 months. A separate pass at 04:40 UTC looks back 7 days once a day, so a short outage still repairs itself.
  • The expensive half of the work runs hourly instead of every 15 minutes. Hourly figures keep their 15-minute cadence; daily and monthly move to hourly, at :20.
  • Two new database indexes on the two tables the cron reads most.

Why

On production the cron was burning roughly 55 minutes of database time every 6 hours. It re-read between 32 and 62 days of raw data, for all 38 organisations, 96 times a day — to produce monthly figures that only change once a month.

Dashboards look the same and hourly numbers are as fresh as before. Daily and monthly numbers can now be up to an hour behind.

The tradeoff worth knowing about is not the lag, it's the window. Monthly is now the sum of the daily rows, so a record that only becomes final more than 7 days after it was created never lands in a daily row, and therefore never lands in the monthly total. That is an under-count, not a delay. We measured it: across 405,951 rows, zero took longer than 7 days. It stays safe as long as that holds, so the rollup now reports any monthly total it lowers, and README.md documents the same limit.

How

  • _rollup_monthly_from_daily replaces the per-org monthly source queries with one INSERT ... ON CONFLICT DO UPDATE over event_metrics_daily for every org. Upsert-only, per the design agreed on UN-3973: a monthly row the daily tier no longer produces is left in place, because a stale total is recoverable with backfill_metrics and a deleted one is not.
  • aggregate_metrics_from_sources(tier, source_window_days) is called by three schedule rows. Both kwargs travel on both transports — Beat calls the Django task directly, the PG scheduler goes worker proxy → internal endpoint → the same function.
  • Lock keys are one per granularity written, namespaced by source window. ALL takes both, so it excludes a concurrent hourly run at the same window; distinct windows are distinct jobs, so the once-daily reconciliation pass can never be starved by the 15-minute schedule. Across windows the lock deliberately does not exclude — the reconciliation row is scoped to daily_monthly so it no longer writes event_metrics_hourly alongside the */15 run, and the consumer's concurrency of 1 serialises what remains. docker-compose.yaml states that mitigation; the cloud chart's matching comment is corrected in unstract-cloud#1762.
  • Both indexes are built CONCURRENTLY under atomic = False, with AddIndex confined to state_operations. Each migration asserts the index is valid and has the expected definition before recording itself applied.

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

  • Monthly goes stale if the daily tier has a gap. Gaps shorter than 7 days repair themselves on the next reconciliation pass; anything older needs backfill_metrics. Bounded to the current and previous month.
  • Rolling the code back past this release breaks aggregation until migration 0006 is reversed — the schedule rows carry a tier argument the previous release rejects. migrate dashboard_metrics 0004 restores it. 0005 is not far enough: its reconciliation row carries source_window_days, which the previous release rejects the same way.
  • A pod still on the old image mid-rollout receives tier and raises TypeError until it rolls. Self-healing — no aggregation is lost, the next tick succeeds.
  • Both index builds are non-blocking, and both migrations are reversible.

Database Migrations

Four, in three apps. No schema changes to any metrics table.

Migration What it does
dashboard_metrics/0005_add_reconciliation_task Adds the 04:40 UTC reconciliation schedule, on both Beat and the PG scheduler
dashboard_metrics/0006_split_aggregation_schedule Splits the aggregation into two rows by tier
file_execution/0007_wfe_status_created_idx Index on workflow_file_execution (status, created_at)
workflow_v2/0029_we_created_at_idx Index on workflow_execution (created_at)

Both index migrations no-op via IF NOT EXISTS if the index was built out of band first, which is the preferred production path — the exact statement is in each migration's docstring.

Env Config

  • None.

Deploy Steps

Run once, before the first aggregation after deploy:

python manage.py backfill_metrics --days 62 --skip-hourly --skip-monthly

Monthly is now derived from the daily tier, so that tier has to be complete across the rollup window first. The rollup starts at the first day of the previous month — up to 61 days back, hence 62. --skip-monthly is deliberate: repair daily, and let the rollup derive monthly.

Relevant Docs

backend/dashboard_metrics/README.md — schedules, windows, staleness bounds, and the ownership overlap with backfill_metrics.

Related Issues or PRs

Dependencies Versions

  • No dependency changes.

Notes on Testing

Ticket Acceptance criterion State
UN-3973 Monthly derived from event_metrics_daily, not source tables Met
UN-3973 Per-run daily source window is 2 days Met
UN-3973 A once-daily 7-day reconciliation pass exists and is scheduled Met
UN-3973 Daily and monthly match pre-change values, incl. across a month boundary Met
UN-3973 Tests cover the month boundary and the reconciliation pass Met
UN-3972 Index present, indisvalid = t Met
UN-3972 Non-atomic + CONCURRENTLY, no write-blocking lock Met
UN-3972 get_documents_processed free of a seq scan on workflow_file_execution Confirm on prod
UN-3972 get_failed_pages free of a seq scan on workflow_execution Met
UN-3972 get_recent_activity under 1 s Flagged — belongs to the (created_at DESC) index descoped in comment 45015
UN-3974 Hourly stays 15 min, daily/monthly go hourly Met
UN-3974 Hourly figures unchanged, daily/monthly lag ≤ 1 h Met
UN-3974 Prefilter leaves the Query Insights top 10 Confirm on prod

The two Confirm on prod rows are post-deploy readings against Query Insights, not outstanding work.

Screenshots

Not applicable — no UI change.

Checklist

I have read and understood the Contribution Guidelines.

🤖 Generated with Claude Code

https://claude.ai/code/session_017yVv7w8VuDxmDg25a1Encv

…from the daily tier (#2255)

* UN-3973 Derive monthly metrics from the daily tier, narrow source window to 2 days

The dashboard aggregation widened its DAY-granularity query to the first of
the previous month so monthly buckets could be summed in Python from the same
rows. Every run re-read 32-62 days of source data per metric, per org, 96
times a day.

Monthly is now rolled up from event_metrics_daily in one statement for all
orgs, so the source queries only need the daily window. That window drops to
2 days, sized against the measured worst created_at -> terminal-status lag of
~2h. A once-daily 7-day pass reruns the same task at a wider bound to repair
gaps left by cron downtime.

The active-org prefilter is decoupled from the daily window and pinned at 7
days: metrics filtered on another column (hitl_completions on approved_at) can
land for an org whose executions are older than the source window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3973 Address Sonar and Greptile review findings

Sonar:
- S117: rename apps.get_model() locals in 0004 to snake_case
- S3776: cut _run_aggregation cognitive complexity from 22 by hoisting the
  static metric config tables to module level and extracting the per-org
  body, the active-org prefilter and the result shape into helpers

Greptile:
- Monthly rows in the rebuilt window whose daily rows are gone are now
  deleted alongside the upsert, so the two tiers cannot disagree. An empty
  daily tier still short-circuits, so a wiped tier cannot cascade into
  deleting monthly history.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3973 Add tests covering the source window, rollup SQL and reconciliation schedule

Closes the acceptance criteria that had no automated check:

- the monthly rollup issues no source-table SQL, asserted by capturing the
  queries it actually sends
- the window ladder at 2 / 7 / 62 days, including a row that finishes after
  the narrow window has moved past its created_at and so never re-enters it
- the reconciliation schedule row, its idempotency and its reverse

The schedule tests call the migration's function directly. The suite runs with
--no-migrations, so data migrations never execute and asserting on the beat row
would fail regardless of the migration being correct.

Also moves the dotenv load in settings/base.py above the Celery block.
CELERY_BROKER_BASE_URL, _USER and _PASS were read above it, so they could not be
supplied by an env file at all and had to be ambient. Ambient values still take
precedence, so deployed behaviour is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3973 Trim comments in tasks.py and revert the unrelated settings change

Cut the verbose comments and docstrings down to the purpose and the
non-obvious bits. Code is unchanged.

Restore backend/settings/base.py to main — moving the dotenv load ahead of
get_required_setting was a local test convenience, not part of this change.
The test rig exports the broker vars itself, so CI never needed it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3973 Renumber the reconciliation migration to 0005

UN-3445 landed 0004_pg_periodic_tasks on main after this branch was cut, leaving
dashboard_metrics with two 0004s depending on 0003 and nothing depending on either.
Django saw two leaf nodes and refused to build the graph, so `migrate` failed before
applying anything — every app, not just this one.

Depend on 0004_pg_periodic_tasks and renumber to match, so the short prefix form
stays usable for a rollback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3973 [FIX] Address review: PG transport, scoped orphan sweep, retry posture

The reconciliation row could not run on the PG transport — two functions share the
task name dashboard_metrics.aggregate_from_sources and the worker one took no
arguments, so the mirrored row dispatched source_window_days into a zero-arg function
and the message was dropped. The worker proxy and the internal endpoint now plumb it,
and 0005 declares the PG twin rather than leaving the mirror to invent one.

The orphan sweep is scoped to the (organization, month) partitions the rollup actually
produced: an incomplete daily tier passed the empty-tier guard and deleted monthly rows
it could not vouch for. Its deletion count now reaches the task result and a WARNING.

DatabaseError and OperationalError propagate from the monthly rollup so the configured
autoretry fires, instead of being logged once behind success: True.

The prefilter is never narrower than the query window, so a widened source_window_days
cannot skip the orgs it exists to repair. bulk_create takes an explicit batch_size.

The Beat/PG drift guard named 0002 and 0004, so it kept comparing three schedules
against three while this PR added a fourth. It now discovers every migration in the
app, replays their RunPython forwards in order, derives the Beat cadence from the
schedule row, binds every declared kwarg to its task signature, and asserts every
post-install Beat write bumps PeriodicTasks.last_update.

The reconcile row moves to 04:40 UTC: */15 fires at minute 0, and the per-tier lock
keys do not block each other, so 04:00 started two full aggregations at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3973 [FIX] Address Athul's review: upsert-only monthly, per-schedule lock, honest success

The orphan delete is removed. The design agreed on this ticket (comments 44768/45016)
is a pure INSERT ... ON CONFLICT DO UPDATE; the delete was scope beyond it, and it
converts a recoverable undercount into unrecoverable loss — the daily rows that would
rebuild a deleted monthly row are exactly the ones that were missing. A stale total is
recoverable with backfill_metrics.

The reconciliation pass no longer shares a lock key with the 15-minute schedule. The
15-minute row is an IntervalSchedule and drifts against a fixed crontab, so on a shared
key the once-daily repair loses the race roughly one day in seven, returns
skipped=True and is never retried.

A run in which every metric for every org failed no longer reports success: True. The
result's success now reflects the error count, the completion log rises to WARNING, and
the worker-side guard reads skipped_reason and errors as well as skipped — it saw none
of these three did-nothing shapes before.

A failed monthly rollup is distinguishable from an empty one: upserted=0 collided with
the legitimate no-op and the no_active_orgs return.

source_window_days is validated and bounded. It arrives as JSON from a Beat row that is
editable in the admin: negative puts the window in the future, 0 never refreshes
yesterday, 365 restores the multi-month scan this ticket exists to remove.

Tests: a golden test seeds source rows, lets the real aggregation populate daily, and
compares the rolled-up monthly against the pre-change derivation computed independently
from get_documents_processed — AC-4 was claimed Met and had no equivalence assertion.
Fixture offsets derive from the month boundary rather than fixed day counts, which land
in the wrong month for the last days of any month.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ecution on (status, created_at) (#2264)

* UN-3972 [PERF] Index workflow_file_execution on (status, created_at)

The dashboard metrics cron's documents_processed and failed_pages queries
filter this table on status + a created_at window, but all four existing
indexes lead with workflow_execution_id. With no entry point here the planner
drives top-down from the org and sequentially scans all 1.28M rows of
workflow_execution — 83% of the cron's DB time on production.

Built CONCURRENTLY with atomic = False; a plain AddIndex would hold a SHARE
lock over a 3.4GB table taking live inserts. Guarded against a leftover
INVALID index from an interrupted build, which IF NOT EXISTS would otherwise
keep silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3972 [PERF] Trim the migration docstring to the project ceiling

The docstring restated the prod plan, deployment runbook and recovery steps.
That detail belongs in the PR, not in a file every future agent scans.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3972 [PERF] Guard the index migration's non-atomic CONCURRENTLY shape with tests

The suite runs with --no-migrations, so 0007 is never executed in CI. Regenerating it
with makemigrations, or dropping atomic = False / CONCURRENTLY while tidying, would land
a plain AddIndex — a SHARE lock held for the whole build on a 3.4 GB table that takes
live inserts — with every test still green.

Five DB-free assertions on the migration module and the model's Meta.indexes: non-atomic,
concurrent in both directions, the INVALID-index guard present, AddIndex confined to
state_operations, and model/migration agreement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3972 [FIX] Assert the index definition, not just its validity, and pin reversibility

The CREATE INDEX CONCURRENTLY IF NOT EXISTS matches on name alone, so a hand-built
index with different columns was kept while Django recorded (status, created_at) into
model state — a permanent, invisible divergence that makemigrations --check cannot
see. The guard now compares pg_get_indexdef against the expected btree definition and
qualifies the lookup by current_schema(), since app tables live in the unstract schema.

Also pin that every database_operation is reversible: dropping the guard's
reverse_sql=noop killed the whole rollback path with all five tests still green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3972 [FIX] Address Athul's review: guard semantics, whole-migration assertions

Three mutations that were green are now caught: appending a bare AddIndex after the
SeparateDatabaseAndState (a real lock-taking build on a 3.4 GB table, invisible because
every assertion read operations[0]); flipping the guard's NOT indisvalid polarity, which
either raises on every healthy deploy or never fires at all; and a typo in reverse_sql,
which makes rollback a silent no-op through IF EXISTS while Django unapplies the
migration.

The CREATE assertion matches the column order by regex instead of an exact byte
sequence — removing one space used to fail it, a false-failure mode whose only outcome
is someone loosening the assertion.

Docstrings: the plan citation now points at UN-4045, which supersedes the earlier
workflow_file_execution reading; "every existing index leads with workflow_execution_id"
was false (the PK leads with id); the exact CREATE statement an operator should run out
of band is spelled out, with a warning off the struck two-index variant; and the models.py
comment no longer implies the index fixes both cron queries when it fixes one until
UN-3973 narrows the window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…edule by tier and indexing workflow_execution on created_at (#2265)

* UN-3973 Derive monthly metrics from the daily tier, narrow source window to 2 days

The dashboard aggregation widened its DAY-granularity query to the first of
the previous month so monthly buckets could be summed in Python from the same
rows. Every run re-read 32-62 days of source data per metric, per org, 96
times a day.

Monthly is now rolled up from event_metrics_daily in one statement for all
orgs, so the source queries only need the daily window. That window drops to
2 days, sized against the measured worst created_at -> terminal-status lag of
~2h. A once-daily 7-day pass reruns the same task at a wider bound to repair
gaps left by cron downtime.

The active-org prefilter is decoupled from the daily window and pinned at 7
days: metrics filtered on another column (hitl_completions on approved_at) can
land for an org whose executions are older than the source window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3973 Address Sonar and Greptile review findings

Sonar:
- S117: rename apps.get_model() locals in 0004 to snake_case
- S3776: cut _run_aggregation cognitive complexity from 22 by hoisting the
  static metric config tables to module level and extracting the per-org
  body, the active-org prefilter and the result shape into helpers

Greptile:
- Monthly rows in the rebuilt window whose daily rows are gone are now
  deleted alongside the upsert, so the two tiers cannot disagree. An empty
  daily tier still short-circuits, so a wiped tier cannot cascade into
  deleting monthly history.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3973 Add tests covering the source window, rollup SQL and reconciliation schedule

Closes the acceptance criteria that had no automated check:

- the monthly rollup issues no source-table SQL, asserted by capturing the
  queries it actually sends
- the window ladder at 2 / 7 / 62 days, including a row that finishes after
  the narrow window has moved past its created_at and so never re-enters it
- the reconciliation schedule row, its idempotency and its reverse

The schedule tests call the migration's function directly. The suite runs with
--no-migrations, so data migrations never execute and asserting on the beat row
would fail regardless of the migration being correct.

Also moves the dotenv load in settings/base.py above the Celery block.
CELERY_BROKER_BASE_URL, _USER and _PASS were read above it, so they could not be
supplied by an env file at all and had to be ambient. Ambient values still take
precedence, so deployed behaviour is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3973 Trim comments in tasks.py and revert the unrelated settings change

Cut the verbose comments and docstrings down to the purpose and the
non-obvious bits. Code is unchanged.

Restore backend/settings/base.py to main — moving the dotenv load ahead of
get_required_setting was a local test convenience, not part of this change.
The test rig exports the broker vars itself, so CI never needed it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3974 [PERF] Split the dashboard metrics schedule by tier and index workflow_execution on created_at

Schedule split. One schedule ran every 15 minutes and wrote all three metric
tiers. Dashboard daily and monthly figures do not need 15-minute freshness, so
they move to hourly — 96 runs a day becomes 24 for the expensive
DAY-granularity half of the work, while the hourly tier keeps its cadence.

Both schedule rows point at the same task and differ only in a `tier` kwarg; a
second task name would need its own worker registration and internal endpoint
for the PG path. The lock is now keyed per tier, so the two runs that collide
at the top of every hour do not starve each other. Omitting `tier` still writes
all three tiers, so a manual trigger never silently writes nothing.

Prefilter index. The active-org prefilter measures 1,849ms per call on
production — the slowest single query on the instance. Nothing on
workflow_execution leads with created_at: the two composite indexes are
date-ordered only within one workflow or pipeline, and the partial index is
empty in steady state. The split raises this query's call count, and UN-4045
will leave three more metric queries on the same bare date-range shape, so the
index lands with the split rather than after it.

Built CONCURRENTLY with atomic = False and guarded against a leftover INVALID
index, matching migration 0026.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3974 [PERF] Keep scheduler ownership out of the split migration and cut _run_aggregation's complexity

Migration 0005 used update_or_create for the PG row of the schedule it was only
re-keying, which reset pg_owned to False. converge_pg_scheduler disables a row's
Beat twin when the PG scheduler adopts it, so on an adopted deployment the
migration would have left the aggregation with no firer at all — Beat disabled,
PG no longer owning it. It now updates only task_kwargs on that row, leaving
enabled and pg_owned to the scheduler that owns them. Rollback is symmetric.

Threading the tier through _run_aggregation took its cognitive complexity from
25 to 27 against a limit of 15. Extracted _collect_org_metrics and
_aggregate_org, and hoisted the two static metric tables to module level so they
are not rebuilt per call. Names match the same extraction on #2255 so the two
reconcile cleanly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3974 [PERF] Trim comments and docstrings to the project ceiling

The two index migrations carried 50-60 line docstrings restating the prod plan,
deployment runbook and recovery steps. That detail belongs in the PR, not in
files every future agent scans. Cut to purpose and key behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3974 [PERF] Cover all three acceptance criteria with tests

The suite runs with --no-migrations, so neither 0005 nor 0029 ever executes in
CI, and nothing pinned the schedule split's behaviour at all. 46 tests, at least
one per acceptance criterion.

AC-1 — cadence, and the tier reaching the task. 0005 creates one row and rewrites
one, both scheduler tables agreeing, and the rewrite touches neither pg_owned nor
enabled: on an adopted deployment converge_pg_scheduler has already disabled the
Beat twin, so handing ownership back would leave the aggregation with no firer.
Separately the internal endpoint and the worker proxy are pinned to carry `tier`
— that leg fails silently, since _call_internal builds a body only when a tier is
given and the existing worker test called the task without one.

AC-2 — the split changes no figure. Runs the real _run_aggregation three times
and diffs the metrics tables: `hourly` reproduces the pre-split hourly figures
exactly, and hourly + daily_monthly reproduce every row `all` writes. Two guards
keep it from going vacuous, the second because mutation testing caught the first
version passing while _aggregate_single_metric was broken — the fixture produced
only LLM metrics, leaving half the split unverified.

AC-3 — the index. Migration shape (non-atomic, CONCURRENTLY both directions, the
INVALID guard, AddIndex confined to state_operations), plus an integration test
that EXPLAINs the query the aggregation actually issues, captured rather than
rewritten: a hand-copied queryset would keep passing after the prefilter changed,
which is the one thing it is for. Rows are inserted in ascending created_at order
so the heap matches production's append order. The Query Insights half of AC-3 is
a production reading and is deliberately not faked here.

Every test verified to fail when the thing it guards breaks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3973 Renumber the reconciliation migration to 0005

UN-3445 landed 0004_pg_periodic_tasks on main after this branch was cut, leaving
dashboard_metrics with two 0004s depending on 0003 and nothing depending on either.
Django saw two leaf nodes and refused to build the graph, so `migrate` failed before
applying anything — every app, not just this one.

Depend on 0004_pg_periodic_tasks and renumber to match, so the short prefix form
stays usable for a rollback.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3974 Renumber the schedule-split migration to 0006 behind UN-3973's 0005

UN-3445's 0004_pg_periodic_tasks is the parent of both this migration and UN-3973's
reconciliation migration, so landing both would leave dashboard_metrics with two leaf
nodes and no applicable graph.

Depend on 0005_add_reconciliation_task instead, which puts the intended merge order
(UN-3973 then UN-3974) in the graph rather than in the merge queue. This branch cannot
migrate on its own until UN-3973 lands; its tests are unaffected, since the suite runs
with --no-migrations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3973 [FIX] Address review: PG transport, scoped orphan sweep, retry posture

The reconciliation row could not run on the PG transport — two functions share the
task name dashboard_metrics.aggregate_from_sources and the worker one took no
arguments, so the mirrored row dispatched source_window_days into a zero-arg function
and the message was dropped. The worker proxy and the internal endpoint now plumb it,
and 0005 declares the PG twin rather than leaving the mirror to invent one.

The orphan sweep is scoped to the (organization, month) partitions the rollup actually
produced: an incomplete daily tier passed the empty-tier guard and deleted monthly rows
it could not vouch for. Its deletion count now reaches the task result and a WARNING.

DatabaseError and OperationalError propagate from the monthly rollup so the configured
autoretry fires, instead of being logged once behind success: True.

The prefilter is never narrower than the query window, so a widened source_window_days
cannot skip the orgs it exists to repair. bulk_create takes an explicit batch_size.

The Beat/PG drift guard named 0002 and 0004, so it kept comparing three schedules
against three while this PR added a fourth. It now discovers every migration in the
app, replays their RunPython forwards in order, derives the Beat cadence from the
schedule row, binds every declared kwarg to its task signature, and asserts every
post-install Beat write bumps PeriodicTasks.last_update.

The reconcile row moves to 04:40 UTC: */15 fires at minute 0, and the per-tier lock
keys do not block each other, so 04:00 started two full aggregations at once.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3974 [FIX] Address review: Beat reload, inherited ownership, boundary validation

Rewriting live Beat rows through historical models fires no post_save, so
DatabaseScheduler never reloaded: the existing row kept firing with no tier and the
new row never fired at all. 0006 now bumps PeriodicTasks.last_update in both
directions, as scheduler/ownership.py and mirror_pg_periodic_tasks.py already do.

The new row inherits pg_owned and both enabled flags from the row it is split from
instead of hardcoding Beat. In a PG-adopted environment the daily and monthly tiers
had no firer at all while the hourly run still returned success.

It also moves to minute 20. Minute 0 collides with */15 — and so does the suggested
minute 30, since */15 fires at :00 :15 :30 :45 — and the per-tier locks are built so
the two runs cannot block each other.

An unrecognised tier is now rejected in post(), and the blanket except ValueError in
_run is gone, so a ValueError from inside the aggregation reaches the logged 500 path
rather than reading as a bad request body.

Tests: the JSON round-trip assertion was a stdlib tautology that never read what the
migration writes — replaced with an assertion on the updated row, the one firing the
hourly tier in production. The ALL default is pinned off inspect.signature. The RunSQL
table and column are derived from the model rather than grepped. The planner-choice
assertion is deleted: a cost model on 12,000 rows is not production evidence.

Also drops a full Organization count that ran on every tier for one log field, and
gives two test modules the Django bootstrap their siblings carry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3973 [FIX] Address Athul's review: upsert-only monthly, per-schedule lock, honest success

The orphan delete is removed. The design agreed on this ticket (comments 44768/45016)
is a pure INSERT ... ON CONFLICT DO UPDATE; the delete was scope beyond it, and it
converts a recoverable undercount into unrecoverable loss — the daily rows that would
rebuild a deleted monthly row are exactly the ones that were missing. A stale total is
recoverable with backfill_metrics.

The reconciliation pass no longer shares a lock key with the 15-minute schedule. The
15-minute row is an IntervalSchedule and drifts against a fixed crontab, so on a shared
key the once-daily repair loses the race roughly one day in seven, returns
skipped=True and is never retried.

A run in which every metric for every org failed no longer reports success: True. The
result's success now reflects the error count, the completion log rises to WARNING, and
the worker-side guard reads skipped_reason and errors as well as skipped — it saw none
of these three did-nothing shapes before.

A failed monthly rollup is distinguishable from an empty one: upserted=0 collided with
the legitimate no-op and the no_active_orgs return.

source_window_days is validated and bounded. It arrives as JSON from a Beat row that is
editable in the admin: negative puts the window in the future, 0 never refreshes
yesterday, 365 restores the multi-month scan this ticket exists to remove.

Tests: a golden test seeds source rows, lets the real aggregation populate daily, and
compares the rolled-up monthly against the pre-change derivation computed independently
from get_documents_processed — AC-4 was claimed Met and had no equivalence assertion.
Fixture offsets derive from the month boundary rather than fixed day counts, which land
in the wrong month for the last days of any month.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

* UN-3974 [FIX] Address Athul's review: lock covers what is written, both kwargs, graph guard

The lock is keyed by granularity written, not by enum member. ALL took a third key that
excluded nothing, so an ALL run and the scheduled hourly run wrote EventMetricsHourly
concurrently — reachable from the documented manual trigger and from the endpoint's own
"omit tier" contract. ALL now takes both keys and releases whatever it took if it cannot
take them all. Keys are namespaced by source window so the once-daily reconciliation
pass, which is never retried, is not starved by the 15-minute schedule.

source_window_days is accepted on all three legs. 0006 hard-depends on 0005, so the
reconciliation row is a certainty rather than a hypothetical, and this branch's
signatures rejected the kwarg it dispatches.

The tier predicates come from one membership table, so a member added without an entry
raises instead of acquiring the lock, iterating every org, writing nothing and returning
success.

The migration's bulk updates check their row counts. A filtered update matching nothing
reported success while leaving the old row on kwargs="{}" — every tier every 15 minutes
— alongside the new hourly row: strictly more load than before, silently.

tier is validated at the request boundary with a warning log, and an explicit null is
treated as omitted.

New test_migration_graph.py builds the migration graph, which is what catches 0006's
dependency on a node that is not on this branch; --no-migrations means nothing else does.
Lock behaviour is now exercised rather than its key string asserted, merge_schedules has
coverage at all, the equivalence file carries one absolute expectation and a frozen
clock, and the prefilter asserts the index is usable under enable_seqscan=off rather
than that the planner chose it on 12,000 synthetic rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGZBF68CShem3pbUJM2tBc

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; no outstanding correctness, security, migration, or repository-rule issue remains.

Summary

  • Keeps hourly aggregation on its 15-minute schedule while moving daily and monthly work to hourly execution.
  • Adds a seven-day daily reconciliation pass and propagates tier/window arguments through both scheduler transports.
  • Adds guarded, batched monthly rollups from daily metrics and reports incomplete or potentially lowered totals.
  • Improves backfill failure reporting and deploy/rollback documentation.
  • Adds concurrent indexes for the workflow execution source queries and extensive migration, dispatch, aggregation, and planner coverage.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  S[Raw usage and workflow tables] -->|Every 15 min; 24-hour query| H[Hourly metrics]
  S -->|Hourly at :20; 2-day window| D[Daily metrics]
  S -->|Daily at 04:40; 7-day reconciliation| D
  D -->|Current and previous month rollup| M[Monthly metrics]
  B[backfill_metrics] -->|Repair historical daily rows| D
  I[New source-table indexes] --> S
Loading

Comment thread backend/dashboard_metrics/tasks.py
…e metrics docs

Remediation pass over #2276. Fixes only — no aggregation behaviour changes.

High:
- test_aggregation_tier.py needed a live Redis but sat in the unit tier, which
  provides none: 8 errors, `test (unit)` red. Settings never override CACHES, so
  the suite inherits production django_redis. The lock cases now pin locmem, which
  has identical add/get/delete semantics; they run in CI for the first time.
- backfill_metrics computed start_date without truncating to midnight while the
  cron truncates, so the oldest day was written as a partial day's bucket. The
  monthly rollup now sums the persisted daily tier rather than recomputing from
  source, so that short value became permanent once past the reconcile window.
- 0006's rollback runbook named `migrate dashboard_metrics 0005` with no ordering.
  0005 and 0006 do not exist in the previous release, so that command errors after
  the image rolls back; and 0005's own row carries source_window_days, which the
  old signature also rejects. Corrected to 0004, reversed before the image.

Medium:
- source_window_days > 90 passed the view and raised inside the task, returning 500
  for a bad request. _int_arg now takes the task's own bound.
- test_a_blocked_run_releases_whatever_it_took never executed the rollback it
  claimed to prove: keys sort daily_monthly first, so ALL failed on its first key
  and the rollback loop ran zero times. Reordered; verified it now fails without
  the rollback.
- 0029's index guard checked validity only, so a hand-built (created_at DESC)
  index passed while Django recorded fields=["created_at"]. Lifted 0007's
  definition and current_schema() checks across, with tests.
- Releasing the lock keys was unisolated: one cache.delete raise replaced a
  completed run's return value and stranded the remaining keys.
- The "every 15 minutes" cadence was restated in five places the split made wrong.
  Removed the cadence from prose rather than restating it; it lives in 0005/0006.
- The rollup docstring and its test claimed a deleted day leaves the monthly total
  in place. It does not — the group survives with a smaller sum.
- README: the 7-day window is a lag ceiling, not only a downtime one; the hourly
  tier never self-repairs beyond 24h; added the deploy backfill at --days 62 (60
  misses a day when run on the 31st).
- docker-compose and the cloud chart both asserted the periodics never overlap and
  the Redis lock self-guards. The split made both false; comments corrected.

Also: ruff 0.3.4 (the pinned gate) reported 13 errors and 8 unformatted files, all
in tests this PR adds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aja2YP7hoVWqocsUd6jCkN
…ation reverse on the operation

Two siblings of fixes in 3a49d7479 that the first pass missed.

- TestTheLockIsPerSchedule (test_tasks.py) takes un-namespaced lock keys on the real
  django_redis cache and brackets each test with cache.clear(), which django-redis
  implements as FLUSHDB — it wipes every key in that database, and the Celery broker
  shares db 0 in the test env. Pinned to locmem like its sibling in
  test_aggregation_tier.py. Verified: the class fails 4/4 without the override on an
  unreachable Redis and passes 4/4 with it.

- test_it_builds_and_drops_concurrently greps the migration source, and the guard
  added in 3a49d7479 put "DROP INDEX CONCURRENTLY IF EXISTS" into two RAISE EXCEPTION
  messages — so the assertion held whatever reverse_sql was. Asserted on the rendered
  operation, as the 0007 sibling already does. Verified: mutating reverse_sql to
  RunSQL.noop now fails the test; before this it left all nine green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Aja2YP7hoVWqocsUd6jCkN
Three small fixes from a review pass; no behaviour change on an install with
active organizations.

_run_aggregation returned on no_active_orgs before reaching the monthly rollup.
That was free while monthly was computed inside the per-org loop, but UN-3973
made the rollup org-agnostic — it sums the whole daily tier and takes no org
argument — so the early return now skips it entirely. An install with no
workflow execution in the prefilter's 7-day lookback never derives monthly,
including on the documented recovery path, where backfill_metrics repairs the
daily tier and the next pass is supposed to roll monthly up from it.

test_wfe_status_created_idx.py imported a model at module scope without the
django.setup() bootstrap its 15 sibling modules carry, so collection aborted
unless another module had already initialised Django. Running that directory on
its own failed, and the full run only worked because dashboard_metrics/tests is
listed first. CI is unaffected — it sets DJANGO_SETTINGS_MODULE — so this is a
local and IDE break only.

Removed a test docstring claiming an "orphan cleanup" bounded to the rebuilt
window. _rollup_monthly_from_daily performs no deletion at all; the docstring is
a leftover from an orphan-delete that was removed under review, and the test it
sits on asserts the opposite property. The test name already states it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFv4oHBnWPThh9wAuufQeX
Comment thread backend/dashboard_metrics/tasks.py Outdated
… empty

_run_aggregation returned early on an empty prefilter, before the monthly
rollup. On main that was correct: monthly was written by _bulk_upsert_monthly
inside the per-org loop, so no active organizations meant nothing to write.
This PR moves monthly out of the loop into _rollup_monthly_from_daily, which
takes no org argument and sums the whole daily tier — so the early return now
skips work that is not org-scoped, and the regression is this PR's own.

It lands on the deploy path the README documents: backfill_metrics
--skip-monthly repairs the daily tier and leaves monthly to the rollup, which
never runs on an installation with no workflow execution in the prefilter's
7-day lookback. Reproduced against Postgres — daily summing to 30.0 with
monthly left at 20.0, and the same sequence self-correcting to 30.0 with one
recent execution present.

Removes the early return rather than adding a second rollup call site, so the
tier guard stays in one place and a later exit cannot skip it again. An empty
id__in compiles to EmptyResultSet and issues no query, so the loop costs
nothing when the shortlist is empty. skipped_reason is still reported from the
prefilter, since that fact is unchanged and independent of what was written.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFv4oHBnWPThh9wAuufQeX
Comment thread backend/dashboard_metrics/tasks.py
…ork"

d248fda made the monthly rollup run when the active-org prefilter is empty.
Before it, skipped_reason could only appear alongside zero rows written — the
early return preceded both the org loop and the rollup — so "did no work" was
always true. It can now be false: a quiet installation upserts monthly from
existing daily rows and still reports skipped_reason=no_active_orgs.

Fixed in the consumer, not the producer. The task's fact is correct — no
organisation had recent activity — and suppressing it when rows were written
would hide the empty-prefilter warning for as long as the rollup still finds
daily rows to sum. The prefilter looks back 7 days and the rollup sums 29-62,
so that gap can run to two months on an installation that has gone quiet.

_log_if_skipped now reports the prefilter reason with the row count, which
distinguishes an empty shortlist that still rebuilt derived tiers from a run
that genuinely wrote nothing. The lock-held and per-metric-error arms are
unchanged, and a healthy run stays silent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CFv4oHBnWPThh9wAuufQeX

@athul-rs athul-rs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Standardized pre-merge review — REQUEST CHANGES

Critical: 0 · High: 5 · Medium: 13 · Low: 10 · Lenses run: 16/16

Reviewed against the team's 16-lens rubric via the PR Review Toolkit specialists (unstract plugin v0.18.1), INITIAL mode, at efe01f2b45.

This is careful work and the docstrings are unusually good — several of the findings below exist because a comment made a precise, checkable claim, which is a better failure mode than most PRs offer. The index migrations in particular are stronger than what UN-3972 specified: the pg_get_indexdef assertion catches the IF NOT EXISTS name-collision case on top of the INVALID case, and the (created_at DESC) index struck in comment 45015 stayed struck.

Nothing here is certain-on-merge data loss, which is why this is not a BLOCK. But five High findings, three of which sit on the deploy path and compound each other, is squarely request-changes.

The three that interact

H1 (tasks.py:208) the monthly rollup blind-overwrites monthly totals from a tier with no completeness check; H2 (0005:82) the reconciliation pass — the only automatic repair for that tier — lands with no firer wherever the periodics are already PG-adopted; H3 (backfill_metrics.py:105) the pre-deploy backfill that is supposed to make H1 safe does roughly double the work it should, inside the window it must beat. Each is individually fixable in a few lines. Together they mean the safety net has a hole, the repair path may never run, and the manual mitigation is slower than advertised.

H4 (tasks.py:758) and H5 (0006:120) are independent.

Unanchored findings

[Medium] [Lens 16] — The PR description contradicts its own README, twice.

  1. "Nothing a customer sees changes... which is the one deliberate trade." README.md:169, added in this same PR, documents that a row whose status turns terminal more than 7 days after created_at is counted in no daily row and therefore in no monthly total — "Before the monthly tier was derived from daily this was caught by the wider monthly source window." That is an unrecoverable under-count of a customer-visible figure, not a staleness trade. The PR's own risk bullet documents a second one. This was flagged on UN-3973 (comment 45708) as being corrected; the README was, the PR body was not.
  2. "ALL takes both, so it genuinely excludes a concurrent hourly run." False for the only scheduled ALL run — the 04:40 reconcile carries source_window_days=7 and no tier, so its keys are disjoint from the */15 run's. docker-compose.yaml:422-425 says the opposite and is correct. See the inline comment on tasks.py:305.

Suggested replacement for (1): dashboards keep the same shape and the same hourly freshness; daily/monthly gain up to an hour of lag; and the correctness envelope narrows from ~32-62 days of lag tolerance to 7 — which the measurement study (zero rows over 7d across 405,951) says is safe today, but is a real silent loss class if that distribution shifts.

Low

_TIER_WRITES with an empty or wrong entry degrades into a false lock_held skip, because _acquire_aggregation_locks([]) returns [] and the caller reads that as contention · "granularity" names the tier labels throughout _aggregation_lock_keys while a real Granularity enum with different members is imported 10 lines away · the DO $$ guards never check indrelid, so a same-named index on another table satisfies both IF NOT EXISTS and the assertion (also _ is a LIKE wildcard in INDEX_DEF_SUFFIX) · 0005:66 hand-writes the Beat JSON string instead of json.dumps(spec["task_kwargs"]), breaking its own "single source for both directions" — CI catches it, which is why this is Low · tier: str is annotated but defaults to an enum member, then validated in three layers · backfill_metrics imports the private _truncate_to_day across a module boundary for what is genuinely a shared contract · the two cleanup views' 400 paths log nothing while the aggregate view's does · the aggregate endpoint silently ignores unknown body keys, so {"teir": "hourly"} returns 200 having run every tier · AGGREGATION_LOCK_TIMEOUT == 900 == the hourly period, so "not outlive the shortest schedule period" holds only at equality.

Lens checklist

# Lens Result
1 Spec & intent See H3, unanchored
2 Architectural fit & precedent See tasks.py:305, Low
3 Correctness & edge cases See H1, H4, tasks.py:640
4 Security Clean — no new auth surface; internal endpoint pre-existing with boundary validation; _base_manager org-scope bypass deliberate and unchanged for a cron context; no secrets, no PII in new logs
5 Data integrity & migrations See H1, H2, H5, 0007:17. Migration graph verified conflict-free against origin/main
6 Concurrency See tasks.py:305, tasks.py:361
7 API & contract compatibility See H5, tasks.py:663
8 Reliability & resilience See H1, H4
9 Performance & cost See H3, tasks.py:746
10 Observability See tasks.py:663, dashboard_metrics_tasks.py:107
11 Operational safety See H2, H5, 0007:17
12 LLM/agent N/A — no model calls, prompts, tools or agent paths; get_llm_metrics_combined aggregates usage rows
13 Testing See the four test-file comments
14 Dependencies & build N/A — no dependency, lockfile, Dockerfile or CI manifest touched
15 Code quality See Low
16 Doc & comment accuracy See 0007:17, tasks.py:663, README.md:168, unanchored

Open questions

  • Has mirror_pg_periodic_tasks --adopt already run for the dashboard_metrics_* periodics in prod? That decides whether H2 is live or latent.
  • SELECT date, count(*) FROM event_metrics_daily WHERE date >= date_trunc('month', now()) - interval '1 month' GROUP BY 1 on prod would bound H1 exactly.
  • Does the deploy pipeline run migrate as a pre-deploy job, and are rollbacks automated? That sets H5's real severity.

Comment thread backend/dashboard_metrics/tasks.py
Comment thread backend/dashboard_metrics/migrations/0005_add_reconciliation_task.py Outdated
Comment thread backend/dashboard_metrics/management/commands/backfill_metrics.py Outdated
Comment thread backend/dashboard_metrics/tasks.py
Comment thread backend/dashboard_metrics/tests/test_tasks.py Outdated
Comment thread backend/dashboard_metrics/management/commands/backfill_metrics.py
Comment thread backend/dashboard_metrics/README.md Outdated
…d stop the retry amplification

Addresses the standardized review's five High findings.

H-D: _roll_up_monthly re-raised DatabaseError/OperationalError to reach the task's
autoretry_for. That helps on neither transport: on the internal-HTTP path Celery's
Task.retry re-raises under called_directly so nothing retries, and on Celery each
retry re-runs the entire aggregation (three more full passes in seconds) against a
database that just reported it is struggling. Counted instead, which already sets
success: False.

H-A: the rollup overwrites a month from a daily tier that may not cover it, changing
values without changing the row count, and no signal existed. _months_missing_days
reports months covered short, and the worker names them.

H-B: --skip-hourly guarded only the upsert, so the mandatory pre-deploy backfill
issued its HOUR source queries anyway over a multi-week window.

H-C: schedule rows are written by a migration in the backend image while the consumer
ships in another, so a row can carry a kwarg the running signature predates. Both
signatures now accept unknown kwargs; the once-daily reconcile row has no next tick.

H-E: the active-org lookback floor had no test that could fail — removing it left the
suite green. Added the 3-7 day case, the only region the floor governs.

Also: independent conditions in the worker's run summary (an error no longer hides
behind "no active orgs"), a zero-rows arm, cleanup failures surfaced, the rollup
streamed rather than materialised per tenant, and doc corrections — the hand-run
CREATE INDEX statements need search_path set, the README's _base_manager tenant
claim, the terminal-status claim (true for 2 of 9 metrics), the clock-skew claim,
and the */15 grid premise, which holds on the PG scheduler but not on Beat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yVv7w8VuDxmDg25a1Encv
…g date fleet-wide

The previous commit's incomplete-month check was wrong at both ends, as an
adversarial verification pass demonstrated against a real database.

It missed the case that matters. It counted distinct dates across every
organization while the rollup writes per (organization, month, metric), so one
tenant losing a day was invisible whenever any other tenant covered it — which is
exactly the shape _collect_org_metrics produces when one tenant's metric query
fails and the run continues. Probe: a tenant's monthly total fell 80 to 70 with
errors 0 and no log line.

And it fired when nothing was wrong: a day on which no organization was active,
a fresh install, or the 1st of a month all read as gaps, so ~25 warnings a day on
a correct daily tier — naming a repair that cannot clear them, and contradicting
README.md:168.

Replaced with the exact condition: snapshot the monthly totals, roll up, report
any that fell. No calendar heuristic, so an idle day and a fresh install are
silent by construction.

Also from the same pass: the streamed rollup is back inside one transaction (a
single bulk_create wrapped all its internal batches in one, so the split traded an
atomic rewrite for a partial one); dropped schedule kwargs are logged, since the
tolerance added for rolling deploys would otherwise hide a mistyped row and run the
reconciliation pass at the 2-day default; the zero-rows warning is raised in the
backend, which both transports reach, rather than only in the PG proxy; unknown
request-body keys are rejected instead of silently ignored; and 0006's module
docstring no longer states the schedule separation unconditionally.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yVv7w8VuDxmDg25a1Encv
…achable guards, deterministic tests

Reconciliation row: scoped to daily_monthly. Without a tier it ran ALL, and its
hourly half was both duplicated work and the only thing writing event_metrics_hourly
concurrently with the */15 run, whose per-tier lock cannot block it. Its Beat kwargs
now come from the same spec dict as the PG row rather than a hand-written JSON
string, which is what that dict says it is for.

Lock: the value carries an owner token, and release only deletes a key this run
still owns — previously a run whose lock had expired would delete whichever run
took the key next. The age-based reclaim is now scoped to pre-token values, which
is what it was written for; a token-bearing lock is recovered by its TTL only. A
fresh pre-token lock still blocks, so a rolling deploy does not steal from an old
pod that is still working.

Tests that could not fail:
- _SplitRecorder.first() ignored its filter and update() reported a match for a row
  that was not there, so 0006's ownership inheritance and its forward guard were
  both unreachable. Both now behave as a queryset does. One test asserted the new
  row lands Beat-enabled when the row it splits is missing — an outcome the guard
  makes impossible; it now asserts the RuntimeError the code actually raises.
- Four suites derived a window from one clock reading and compared it against a run
  that took another, so they failed on a day or month boundary with no code change.
  All pinned to one reading, with a probe that runs them at both boundaries.
- wfe_status_created_idx was verified only as file text. Added the planner test its
  sibling index already had, plus the indisvalid assertion UN-3972 asks for and
  nothing checked. Scoped to the predicate rather than the joined metric query,
  since which side the planner drives from is not something a synthetic fixture pins.

Comments: the lookback floor no longer claims to rescue approved_at metrics, which
it cannot; `success` no longer claims to fail the call, which it does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yVv7w8VuDxmDg25a1Encv
Closes the last three review sub-items.

README gains the rollback procedure beside the backfill step: 0005/0006 write task
kwargs the previous release's signatures reject, so reverting the image alone is not
enough — migrate dashboard_metrics 0004 must run from the outgoing image first, which
a platform-driven rollback skips by construction. The release blocks automated
rollback and now says so where the ops content already lives.

The "run first!" quick-start line pointed at --days=30 rather than the deploy-critical
form; it now points at the Deploy Steps section.

backfill_metrics gains the two tests its changed window arithmetic asked for: the
oldest covered day counts its whole day (red on an untruncated boundary, verified),
and --skip-daily without --skip-monthly emits its warning.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yVv7w8VuDxmDg25a1Encv
Comment thread backend/dashboard_metrics/tasks.py Outdated
The fixture spread created_at with an unqualified UPDATE. It was harmless only
because the test database is empty; run against a populated one it would rewrite
created_at for every row in workflow_file_execution.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yVv7w8VuDxmDg25a1Encv
…hotting them

The under-count check added in bbac5c6 loaded every tenant's monthly rows into a
dict before the rollup, held it across the rollup, and read them all again after.
That compared the right thing on the wrong axis: it scales with organization x
metric x project x tag, which is the axis the streaming rollup two functions below
exists to keep off the heap, and it runs hourly — synchronously inside a request
worker on the PG transport.

Replaced with one statement evaluated in the database, run before the upsert while
the stored value is still the old one, returning only the (org, month) pairs whose
total would drop. In a healthy install that is no rows.

Behaviour is unchanged: the per-tenant regression test still passes, and two new
tests pin the bound — one query regardless of tenant count, and the comparison
happening in SQL. Reverting to the snapshots fails both while the correctness test
still passes.

Reported by Greptile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yVv7w8VuDxmDg25a1Encv
Comment thread backend/dashboard_metrics/tasks.py Outdated
… rollup

Regression from bcdd314. Making the check a single pre-upsert query put it inside
the rollup's own try, so a failure in the diagnostic aborted the upsert that was
about to run — leaving monthly stale for a run that would have succeeded, and
reporting it as a failed rollup.

This had already been fixed once, by giving the diagnostic its own try, and was
reintroduced because nothing tested it. It is now tested: a diagnostic raising
DatabaseError must still let the rollup run, and must not set monthly.failed or
count an error. Both fail if the two share a try again.

Reported by Greptile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yVv7w8VuDxmDg25a1Encv
A 12-agent review over the loop-authored tree, with the lenses the loop had been
reading itself dispatched to fresh agents instead. Findings were verified before
fixing; several were measured down or retracted rather than taken on trust.

Guards that did not guard:
- The planner test asserted a hand-copied predicate, so removing the status filter
  from get_documents_processed left it green. It now takes the SQL from the real
  query, and that mutation fails it.
- **_ignored made inspect.signature().bind() accept anything, silently voiding all
  three "declared kwargs bind to the task signature" guards — on the first PR to put
  kwargs in schedule rows. They now check named parameters; a misspelled kwarg in
  0005 fails them again.
- The release ownership check, the lowered_months propagation and the diagnostic's
  unavailable marker each had no test that could fail. All three now do.
- The backfill truncation test read the real clock and passed with the bug present
  before 01:30 UTC. Frozen.
- The maximum bound on source_window_days had no test.

Signals that did not signal:
- A failed under-count check set lowered = [], indistinguishable from "checked,
  nothing lowered", and the backend's log is in a different process from the
  consumer watching the schedule. The payload now carries lowered_check.
- monthly.failed was written and read by nothing, so a fleet-wide rollup failure
  read as one org's metric error. The worker now reports both.
- "tier wrote no rows" fired on any quiet install, because the prefilter shortlists
  7 days while the hourly tier queries 24h. Scoped to daily_monthly.

Lock:
- Removed the age-based reclaim. It could not tell a live holder from a dead one,
  its read-judge-replace was not atomic, and it was unreachable — the keys are new
  in this release, so no pre-token value can appear under one. Its two tests went
  with it, replaced by the property that is reachable.
- The acquire loop now releases what it took if it raises partway.

Migrations:
- 0005 inherits scheduler ownership instead of hardcoding pg_owned=False. The
  rescue previously relied on is pgScheduleMirror, which ships disabled.
- 0006 creates a missing row instead of raising, which failed migrate for every app
  in the project on a state that cannot cause the harm the guard described.

Docs: the rollback section carries the post-hoc recovery for the case it predicts;
the **_ignored rationale was inverted at three sites; five sites still said the
reconcile pass covers all tiers; the created_at index census was wrong.

Measured down rather than fixed as reported: the diagnostic is 0.3x the rollup at 37
orgs, not 240x (that came from a 300-org fixture) — only the redundant isnull
conjunct was real, and is gone. The deadlock needs a >20 min run (measured 0.5s) and
a concurrency change. backfill_metrics' silent success is a pre-existing swallow.

Verified on a disposable Postgres with migrations applied: both indexes built and
indisvalid, all five schedule rows correct on both transports, reverse to 0004 and
re-apply both clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yVv7w8VuDxmDg25a1Encv
…e design calls

Finishes the standardized review's Medium set. Three of these were judgement calls
rather than patches, so the reasoning is here rather than only in the diff.

The monthly rollup no longer holds one transaction across the whole scan. That
transaction held row locks on every rewritten monthly row until the cursor drained,
and the :20 row and the 04:40 reconcile take different lock keys (namespaced by
window), so two runs could contend for the winner's full rollup or deadlock on an
unordered batch sequence. Each bulk_create is already one statement, so a batch is
atomic without a block; the rows are now ordered so concurrent runs take locks in
the same sequence. What this gives up is an all-or-nothing rewrite, which nothing
needed: every row is correct as of the run, the upsert is idempotent, a partial
rollup is repaired on the next tick, and the merge-base had no transaction here at
all.

The rollup now SKIPS the (organization, month) pairs it would lower rather than
overwriting them. The prescribed pre-deploy backfill could not be sequenced before
the first scheduled run — 0006 creates the :20 row enabled and bumps the Beat
tracker, so it goes live at the end of migrate — which made the runbook a race. The
diagnostic already computed exactly the damaged set before the upsert and then let
the overwrite proceed; keeping those totals turns the backfill into a repair step.
A legitimate decrease is not reachable inside the two-month window, and the warning
still names every pair.

truncate_to_day is public. The day boundary is a contract between the cron and the
repair command, not an internal of either, and the command was importing a
single-underscore name across a module boundary.

Also: the equivalence fixture now passes a window wide enough to reach its -3d and
previous-month rows, which fed no tier at the 2-day default, so AC-2 was proven over
one day of data; _collect_metrics' granularity choice is lifted into a helper rather
than adding a fourth conditional axis to a function already past the repo's stated
limits; the lock-timeout comment no longer claims a bound it only equals, and no
longer implies the Celery ceiling applies on the internal-HTTP path; 0006 no longer
claims the Celery path is serialised by consumer concurrency (workerMetrics runs
--concurrency=2); the calendar probe no longer collects its own loop variables as
test classes; and the prefilter selectivity test reads the lookback constant instead
of a hardcoded interval.

Verified on a disposable Postgres with migrations applied: 1215 backend, 1540
workers; both indexes built and indisvalid; five schedule rows on both transports;
migrate -> 0004 -> migrate round-trips clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yVv7w8VuDxmDg25a1Encv
Comment thread backend/dashboard_metrics/tasks.py Outdated
…he whole org-month

The lowering check compares per (organization, month, metric_name, project, tag) —
the rollup's own conflict key — but returned only (organization, month), and the
rollup skipped on that. One metric with short daily data therefore froze every other
metric for that organization and month, including ones whose totals were fine and
whose update was correct.

Now returns and skips the full conflict key, so a healthy sibling metric still gets
its update while the short one is preserved. The warning still reports at
(organization, month) grain, deduplicated, since that is what an operator repairs.

Pinned: coarsening the skip back to (organization, month) fails the new
test_a_healthy_sibling_metric_is_still_updated.

Reported by Greptile.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yVv7w8VuDxmDg25a1Encv
@athul-rs

athul-rs commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Second review, against e9f919406 — 11 commits past the efe01f2b45 state my earlier review covered. All 24 inline threads from that round are resolved and the fixes are real: pg_owned ownership inheritance in 0005, the rollback documentation, the lock ownership token, and the misleading DASHBOARD_ACTIVE_ORG_LOOKBACK_DAYS comment are all genuinely closed. Lock handling is now correct end to end, and both index migrations' indisvalid + pg_get_indexdef assertions hold up — I traced the interrupted-CONCURRENTLY path and it correctly catches the surviving INVALID index and names the recovery command.

The design remains right and worth shipping. But three of the new fixes introduced or left problems, and the PR's headline safety property — "a monthly total that would be lowered is detected and reported" — does not hold on the two paths most likely to occur in production. Verdict: REQUEST CHANGES, on four items.

1. The under-count guard is a monotonicity check, not a coverage check

_pairs_the_rollup_would_lower (tasks.py:250-252) fires only when a monthly row already exists and the new daily sum is strictly lower than what is stored. Two escapes:

  • No prior row. filter(month__gte=month_start) iterates existing monthly rows. The first daily_monthly run of a calendar month has no row for that month yet, so a gap spanning the month boundary is written short, unguarded.
  • Monotonic escape. The comparison is against the stored value, not against day coverage. Once a short total is stored it only ever grows, so it is never "lowered" again and never re-flagged.

Worked case: an outage 28 Feb 22:00 → 2 Mar 06:00. March 1 is never written for any org the created_at prefilter does not shortlist at 04:40. March's first monthly row is built from days 2–31, unguarded; every later run sums higher than the stored value, so nothing fires. That tenant's March total is permanently short, with errors: 0, success: True, and no lowered_months.

This one is a regression inside the PR, which is why I am flagging it rather than filing it: 2006e8c7f added _months_missing_days, a genuine day-coverage check that caught both escapes. bbac5c65d deleted it in favour of the monotonicity check. The motivation for that swap is legitimate — a fleet-wide date count does flag an idle day or a fresh install — but the replacement does not cover the original's cases, and _months_missing_days has zero occurrences at HEAD.

Cheapest correct fix is Count("date", distinct=True) in the rollup's existing aggregate, compared against days elapsed in that month. That catches the no-prior-row case, the monotonic-escape case, and the currently-caught case in one predicate.

2. The guard fails open, and the run still reports success

tasks.py:861-870. When _pairs_the_rollup_would_lower raises, the handler sets lowered_check="unavailable" but does not increment stats["errors"]. _build_result computes "success": stats["errors"] == 0 (:816), so the run is a success — and lowered_pairs = [] means the rollup proceeds with skip=set(), i.e. with the guard disabled, overwriting every total it would have protected.

The likely trigger is statement_timeout on that correlated subquery, which means it fails on every run rather than once.

I agree with the reasoning in the comment above the block — a broken diagnostic should not abort a rollup that would have succeeded, and splitting the try in b35278896 was right. But "don't block the rollup" and "call it a success" are separable. Adding stats["errors"] += 1 in the except keeps the rollup running, flips success to False, and lights the worker's errors arm, which is the only arm anything alerts on. test_tasks.py:1399 pins this behaviour but never asserts success, because it is currently True.

3. The worker logs the inverse of what the backend logs

workers/scheduler/dashboard_metrics_tasks.py:143-146 reads lowered_months and logs "lowered existing monthly totals for %s". The backend, off the same field, logs "left %s unchanged — the daily tier now sums lower than the stored total, so the figures were kept rather than overwritten. Repair daily for those months with backfill_metrics" (tasks.py:890).

Two processes emitting contradictory alerts about the same event. On the PG transport — the one this release moves toward — the worker line is what on-call sees first, and it asserts a downward overwrite that did not happen. That points at a rollback; the correct response is a backfill. Opposite remediation, and the worker line also drops the remediation instruction the backend line carries.

Worth renaming the payload key too: lowered_months holds the months that were not lowered.

4. backfill_metrics prints SUCCESS and exits 0 when every query failed

backfill_metrics.py:400,433,234. _collect_metrics swallows every metric-query failure into logger.warning — no traceback, no counter — so total_stats["errors"] never rises, and the command unconditionally prints BACKFILL COMPLETE. There is no CommandError and no non-zero exit anywhere in the file.

This matters more than it normally would because it is the PR's mandatory pre-deploy step (--days 62 --skip-hourly --skip-monthly). If it fails wholesale in the deploy window the operator sees green, ticks the runbook, and the fleet lands directly in finding 1. Suggest counting failures in _collect_metrics, switching to logger.exception, and raising CommandError on a non-zero count.

Second tier — worth fixing, not blocking

  • tasks.py:306 — still no NULL-org guard. Latent rather than live (no current writer originates one), but this PR removed the structural impossibility: organization_id used to come from a loop variable and is now read from event_metrics_daily. .exclude(organization_id__isnull=True) closes it permanently. Such rows are also invisible to the lowering guard, since OuterRef on a NULL never matches.
  • tasks.py:738,753,937SoftTimeLimitExceeded subclasses Exception, so the broad per-org catch logs it as a per-org DB error and continues the loop. The hard limit then SIGKILLs 60 s later, finally never runs, and the lock leaks for its 900 s TTL. Re-raising it (and Terminated) before the broad catch restores the graceful unwind the soft limit exists for.
  • workers/scheduler/dashboard_metrics_tasks.py:197-199_run answers 200 for any dict and _log_if_skipped never raises, so a run reporting success: False is still recorded as a succeeded task. Raising on errors > 0 / monthly.failed would make the consumer's counter honest. _log_if_skipped itself is nicely built — independent conditions rather than an elif chain — the issue is that its entire output is prose.
  • 0006:119-172 — the fall-through violates the invariant its own comment states. With the row on only one transport, beat_updated and pg_updated is False and both get update_or_create, rewriting enabled/pg_owned. It also sets crontab= without clearing the IntervalSchedule that 0002 created, leaving a row that fires but raises ValidationError on any later save through the admin. Separately, merge_schedules raises RuntimeError on exactly the install shape the forward path tolerates — during a rollback.
  • tasks.py:262-273"... and more (showing 20)" reports the limit, never the total, so three affected tenants and four thousand read identically. len(seen) is already in scope.
  • README.md:431-440 — the rollback recovery SQL omits SET search_path, so it fails with relation ... does not exist. This PR's own index migrations document that correctly; worth copying the line across.
  • Docs vs e9f919406. That commit inverted the rollup's behaviour and four places did not follow: _rollup_monthly_from_daily's docstring now contradicts itself (para 1 says skipped pairs are left untouched, para 4 still says a partially-covered month is overwritten and merely reported), and README.md:403-408 plus the --skip-monthly / --skip-daily help all promise an under-count the guard now prevents. The README overstating the danger could push an operator into an unnecessary full monthly re-backfill.
  • tasks.py:252new_total__lt=F("metric_value") compares FloatFields with a bare <. Parallel aggregate summation is not order-stable, so re-summing an identical set can land one ULP low, freezing a healthy month permanently. A relative tolerance would avoid it.
  • tasks.py:908hourly_start is untruncated while daily_start gained truncate_to_day in this PR. Same mechanism, bounded impact since hourly is not rolled up and expires at 30 days, but the inconsistency will trip someone.

Tests

Genuinely above the usual bar — test_tier_split_equivalence.py proves the partition against real Postgres rather than against _TIER_WRITES, and the anti-vacuity guards, silence controls and the _SplitRecorder fix are all the right instincts. Four holes worth closing:

  1. The lock round-trip never executes. The one test touching the task's lock path patches the cache wholesale. AGGREGATION_LOCK_TIMEOUT equals the */15 period, so a broken release would silently skip roughly every other hourly run while every run reports success.
  2. The under-count guard is only exercised inside a single month — deleting the per-month correlation in the subquery keeps the suite green. This is what let finding 1 through.
  3. Neither index migration is ever run against Postgres under --no-migrations, so the DO $$ guard block is never parsed by a server.
  4. 0005's ownership inheritance is only tested on the fallback branch. 0006's equivalent is well covered, so this is an asymmetry rather than a gap in approach — but 0005 is the row whose regression strands the only automatic repair path.

Not blocking, for later

tasks.py is 1081 lines against the 800 maximum, with clean seams at locks / tiers / rollup. And since mypy is commented out in .pre-commit-config.yaml, keyword-only arguments on the four aggregation helpers are the only enforcement available against positional swaps — _aggregate_single_metric takes 11 positional parameters including three adjacent datetimes followed by two identically-typed dicts, and transposing either pair produces wrong numbers with no error.

The monthly arithmetic is still provably identical to the old path, and the cost model from the earlier review is unchanged — none of these 11 commits touched query shape, frequency, or either index.

kirtimanmishrazipstack and others added 10 commits September 9, 2026 12:29
…lup guard

Blocking (his 1-4):

1. The under-count guard was a monotonicity check, not a coverage check.
   `_pairs_the_rollup_would_lower` iterates EXISTING monthly rows, so it says
   nothing on the first run of a calendar month, and nothing ever again once a
   short total is stored — every later sum is higher, so it is never "lowered".
   Restores a day-coverage check alongside it (`_months_missing_days`, deleted in
   bbac5c6), reported as `incomplete_daily_coverage`. Fleet-wide, because a
   missing day means the aggregation did not run; per tenant it would flag every
   org that was merely idle. Reported rather than skipped: refusing to write a
   short month leaves dashboards empty rather than slightly low.

2. The guard failed open and still reported success. A failing diagnostic left
   `skip` empty, so the upsert ran with the guard OFF, while `errors` stayed 0 and
   `success` stayed True. Now counted. The two diagnostics moved into
   `_run_diagnostic` so one failing does not disable the other.

3. The worker logged the inverse of the backend. Off the same field it said
   "lowered existing monthly totals" where the backend says the totals were KEPT —
   opposite remediations for one event, and on the PG transport the worker line is
   what on-call sees first. Payload key renamed `lowered_months` →
   `needs_daily_repair`, which is what it holds.

4. `backfill_metrics` printed BACKFILL COMPLETE and exited 0 when every query
   failed: `_collect_metrics` caught each one individually, so none reached the
   per-org handler that owns the counter. Counted, `logger.exception`, and
   `CommandError` on a non-zero count. This is the mandatory pre-deploy step.

Second tier:

- B1 NULL-org rows are now excluded from the rollup source. Worse than reported:
  `unique_monthly_metric` includes the nullable org and PG unique indexes are
  NULLS DISTINCT, so ON CONFLICT never matches and every run INSERTs another
  duplicate. Pinned by a test that runs the rollup three times.
- B2 `SoftTimeLimitExceeded` re-raised ahead of the three broad catches; it
  subclasses Exception, so swallowing it logged a per-org DB error and continued.
- B4b `0006` sets `crontab=` without clearing the `IntervalSchedule` `0002`
  created, leaving a row django-celery-beat rejects on any later save.
- B5 the truncation line reported the cap, never the total.
- B6 the README rollback SQL is hand-run, so it needs `SET search_path`.
- B7 six documentation sites still described the pre-guard behaviour, including
  `_rollup_monthly_from_daily`'s own docstring contradicting its first paragraph.
  Two of the six were found by sweep, not review: the module docstring and the
  runtime warning in `backfill_metrics` both gave advice the guard had inverted.

Waived, with evidence: B3 (the PG consumer records nothing either way for a
fire-and-forget periodic, so raising changes nothing — the two breakage arms are
now `logger.error`, which is the only signal that reaches an alert), B4a and B4c
(the fall-through writes back what it read; the reverse's guard is unreachable on
a shape the forward path produces), B8 (both sums are forced serial — correlated
SubPlans are parallel-restricted and `.iterator()` uses a cursor — so the float
comparison cannot spuriously fire), B9 (`daily_start` was already truncated at the
merge-base; the asymmetry is pre-existing and untouched).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yVv7w8VuDxmDg25a1Encv
mypy is commented out in .pre-commit-config.yaml, so a positional swap has no
enforcement at all. _aggregate_single_metric took 11 positional parameters
including three adjacent datetimes (hourly_start, daily_start, end_date) and two
identically-typed dicts (hourly_agg, daily_agg); transposing either pair produces
wrong numbers with no error and no failing test.

Each of the four helpers now takes one positional subject and nothing else. The
enforcement is structural — Python raises TypeError — rather than a convention.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yVv7w8VuDxmDg25a1Encv
Each gap was reproduced with the mutation the reviewer named, and each new test
was confirmed to die on that mutation while the pre-existing tests stay green.

T1 — the task's lock round trip. His claim is overstated at the helper level:
`_release_aggregation_locks` IS covered, and gutting it kills two existing tests.
What is uncovered is one level up — the task's own `finally`. The only test that
drives `aggregate_metrics_from_sources` through the lock patches both the acquire
helper and `cache`, so replacing the `finally` body with `pass` left the suite
fully green. TestTheTaskAcquiresAndReleasesForReal drives the task against a real
locmem cache: keys held during the run, released after, a re-entrant second run
locked out, and keys released when the run raises. His TTL point is confirmed —
AGGREGATION_LOCK_TIMEOUT and the hourly period are both 900s, so a broken release
skips a run while still returning {"success": True, "skipped": True}.

T2 — the guard was only ever seeded inside one month, so the per-month
correlation was inert. Deleting `.filter(bucket=OuterRef("month"))` leaves the
five existing guard tests green. The rollup window starts at the first of the
PREVIOUS month, so uncorrelated it compares one month's stored total against two
months of daily rows: the sum is always larger and nothing is ever flagged.
Worth noting the mutant also FALSELY flags a healthy month, which makes the
rollup skip a correct month's upsert and freeze it.

T3 — neither index migration's `DO $$` block was ever parsed by a server. Under
--no-migrations the migrations never run, and the existing test asserts on the
SQL as Python substrings, so breaking BEGIN to BEGINN in both files leaves the
suite green while `migrate` would fail on deploy. The new test executes both
guards against Postgres, and exercises the two cases they exist for: an index of
the same name built descending (the slip IF NOT EXISTS cannot catch) and a
missing one.

T4 — 0005's `_inherited_ownership` was covered only on its `is None` fallbacks,
because the existing tests run against a clean DB where the aggregation row does
not exist; hardcoding {True, True, False} passes all four. The new test seeds a
PG-adopted fleet and asserts the reconciliation row follows it. 0006's identical
helper was already covered, which is what made the asymmetry visible.

Not a finding: the "order-dependent suite" reported alongside these. pytest-
randomly is not a dependency (absent from pyproject.toml and uv.lock), so there
is no shuffling; the varying results came from concurrent runs sharing one test
database. Three consecutive runs are identical at 185 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yVv7w8VuDxmDg25a1Encv
Two of these are defects in code the previous commits added, both High.

The coverage check counted today in the numerator while measuring against
yesterday in the denominator, so today's row cancelled exactly one missing
earlier day. Since the daily/monthly schedule runs at :20, today normally has
rows — meaning a SINGLE missed aggregation day, the case the check exists for,
was silent, and it only fired at two or more. It also flapped: the same state
reported a gap at 00:20 and nothing at 09:20. Both sides now count whole days
only. My own tests passed because the fixture stopped at day 9 while "today" was
day 10, so the pair never exercised the production shape — a gap WITH today's
row present. That case is now a test.

_run_diagnostic's broad `except Exception` swallowed SoftTimeLimitExceeded — the
exact hazard the three re-raises in the previous commit exist to close, and this
was a fourth site introduced alongside them. Swallowed, the check returns [] and
the rollup runs its fleet-wide upsert with the guard OFF, overwriting the totals
the guard was preserving, with under a minute left before the hard limit.
Re-raised here and at the rollup's own catch.

Two docstring claims were false and are corrected rather than defended: "a fresh
install is silent" holds only for an install starting on the 1st, and the check
cannot see a day lost by a single tenant or metric — one row from anyone marks
the date covered. The warning no longer prescribes a backfill unconditionally,
because a date on which nothing ran anywhere reads identically and needs none.

Nothing crossed the reporting seam for the new key: returning [] from the check,
or typoing `incomplete_daily_coverage`, left the suite green while the control
typo on `needs_daily_repair` failed. Now covered end to end.

From the simplify pass: `_report` extracts the two identical stats-plus-warning
blocks; `_collect_metrics` returns its failure count instead of writing an
instance attribute, matching `_collect_org_metrics`; the worker binds
`result["monthly"]` once instead of five times and drops a docstring that
enumerated the arms and had already gone stale against them; `lowered_check`'s
log said "lowered" where nothing was lowered; over-long comments cut to the rule
in CLAUDE.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yVv7w8VuDxmDg25a1Encv
The adversarial pass noted B5's message change was invisible to the suite:
reverting `"... and N more of TOTAL"` to the old `"... and more (showing 20)"`
left everything green. Same for the worker's new coverage arm. Both are now
mutation-checked — each fails with its production line reverted and passes with
it restored.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yVv7w8VuDxmDg25a1Encv
The simplify agents read up to ed582c2 and the adversarial verifier read
244853e, so the last two commits had no independent pass. Reading them back:

_run_diagnostic's docstring said it records unavailability "rather than raising",
which stopped being true the moment SoftTimeLimitExceeded was made to propagate
through it. Corrected, and cut to the size of the body.

The comment on the backfill's failure counter referenced "the PR" — meaningless
once merged — and carried "used to" history. Rewritten to state the mechanism.

Left alone deliberately: the same "used to" phrasing in two TEST docstrings. For
a test, the behaviour it guards against is the reason the test exists, and the
surrounding suite already documents regressions that way.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yVv7w8VuDxmDg25a1Encv
…urface

A verifier given the files and one question — "can each actor do what it is told?"
— with the design rationale deliberately withheld. It found five things the
context-carrying passes did not, all on the half nobody had been reading: the
operator's and on-call's.

Verified against a disposable Postgres, not asserted:

The README's contingency recovery is a one-way door. Its SQL changes rows, not
`django_migrations`, so a later roll-forward reports "No migrations to apply" and
restores nothing — measured: 5 rows, recovery SQL, 3 rows, roll forward, still 3.
The install then runs the single */15 row with kwargs '{}', which defaults `tier`
to ALL: every tier 96 times a day, the load this release removes, and no
reconciliation pass, with `tier=all` in a log line as the only trace. Documented,
with the `migrate dashboard_metrics 0004 --fake` step that undoes it — also
measured: 3 rows, --fake, forward, 5 rows with kwargs intact.

The README told the operator to run the backfill "before the first aggregation",
which `tasks.py` says outright cannot be produced — 0006's row goes live at the
end of `migrate`. It now says what actually happens and what it costs.

The Quick Commands block told the operator to run the deploy form BEFORE
deploying. `--skip-hourly` only skips the HOUR queries as of this release; from
the outgoing image it issues them anyway, which is the scan this change exists to
remove.

Two worker arms: the "wrote no rows" warning had no tier guard, where the backend
guards the identical condition with `_writes_daily_monthly` and says why — on the
*/15 row it warns 96 times a day about a healthy weekend. And the
incomplete-coverage line dropped the "needs no action" clause from the backend's
message it claims parity with, telling on-call to run a 62-day backfill that
cannot clear the condition.

Deferred: adding a third pre-rollup check reaches `tasks.py` cleanly and reaches
on-call not at all — every result key is hand-enumerated in the worker, in a
different image, with no shared constant. The fix is a shared key table, which is
a cross-cutting change and does not belong in this PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yVv7w8VuDxmDg25a1Encv
…--help

A second cross-document sweep, this time keyed on the FLAG NAMES rather than on
the behaviour words, found a block presenting itself as `--help` output whose
three `--skip-*` lines still describe the pre-guard behaviour — the same class as
the six sites already fixed, in a place neither the review nor the first sweep
reached because it names no behaviour. It was also missing `--active-only`.

The lines now match the real help, and the block says outright that `--help` is
authoritative, since the caveats on those three flags do not fit one line and
duplicating them is what let this drift in the first place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yVv7w8VuDxmDg25a1Encv
The previous commit corrected the README's reproduction of this flag to "skips
the HOUR source queries, not just their upsert" and labelled `--help` the
authoritative text. The help itself still said "Skip hourly aggregation (only do
daily/monthly)" — so the fix pointed readers at a less accurate statement than
the one it replaced.

The distinction is the whole reason the flag matters to the deploy step: the
window is measured in weeks, and issuing the HOUR queries anyway reinstates the
scan this release removes. The docstring on `_collect_metrics` and the test
`TestSkipHourlySkipsTheQueries` both already say so; only the help did not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yVv7w8VuDxmDg25a1Encv
Two High, and the first one is a correction to my own verification.

The README told an operator who had hand-run the recovery SQL to run
`migrate dashboard_metrics 0004 --fake` before rolling forward. At that moment
the operator is on the OUTGOING image — the paragraph above says so — where
0005 and 0006 are not on disk, so 0004 has no child to unapply and the command
prints "No migrations to apply" while un-recording nothing. I had verified this
step and reported it proven; I verified it on the incoming image, where the files
exist, which is the one image the operator is not standing on. Measured both ways:
files absent -> empty plan; files present -> both unapply. The text now says to
deploy the new image first and run two commands from it, and says what happens if
you run it from the old one.

test_an_incomplete_daily_tier_reaches_the_result_dict skipped only when no whole
day had elapsed, so on the 2nd of every month it ran with exactly one elapsed day,
its single seeded row covered it, and the assertion raised KeyError — the whole
backend suite red for every PR in the repo, one day a month, reading as a rollup
regression. Guard widened; replayed across the 1st, 2nd and 3rd to confirm.

Five lines the diff added were constrained by nothing — each mutated cleanly with
both suites green:
  coverage_check's payload key (its sibling needs_daily_repair was pinned, this
    one was not — the same seam, half-covered)
  the worker's two logger.warning -> logger.error upgrades, invisible because
    every worker test asserts on caplog.text, which cannot see severity
  the LLM arm's failures += 1
  0006's "interval": None (verified on a real database, but nothing in CI held it)
All five now fail under the mutation that exposed them.

Also ran the pinned ruff over the new test file, which it had never seen.

Not fixed, deliberately: the coverage warning repeats ~25x/day for up to 62 days
after a single fleet-idle day. The suggested fix — only report contiguous gaps —
would reintroduce the exact defect this check was added for, since a one-day
outage is indistinguishable from an idle day by contiguity. Needs a product call
about warn frequency, not a patch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017yVv7w8VuDxmDg25a1Encv
@kirtimanmishrazipstack

Copy link
Copy Markdown
Contributor Author

@athul-rs Thanks — this was a genuinely useful second pass. Pushed as 6cc6abece (10 commits on top of e9f919406).

All four blocking items and the second tier are addressed. Five of your findings did not survive measurement, and I've set out the evidence rather than quietly dropping them. Two defects in my own fixes are disclosed at the end — both were caught by independent passes, not by me.

The four blocking items

1. The guard was a monotonicity check, not a coverage check. Confirmed, both escapes. _months_missing_days is back alongside _pairs_the_rollup_would_lower, reported as incomplete_daily_coverage.

I did not take the Count("date", distinct=True) fold. That aggregate groups by the full conflict key, so a day count there is per tenant per metric per project per tag — it fires for every tenant idle on a Sunday and every metric an org doesn't exercise, and it cannot feed skip without freezing healthy rows. The check is fleet-wide instead, because a missing day means the aggregation didn't run, which is fleet-wide by nature. It warns rather than skipping: refusing to write a short month empties dashboards instead of under-filling them, and there is no stored total to preserve in that case.

The limit is now stated in the docstring rather than implied away — a day lost by a single tenant or metric is still invisible to it, and that is what the per-tenant check is for.

2. Fails open and still reports success. Confirmed. stats["errors"] += 1 in the diagnostic's handler. The test you pointed at kept the invariant that actually mattered — a failing diagnostic is not a failed rollup — and a new sibling asserts the half it was silent on, success is False with errors == 1. Both are mutation-checked.

3. The worker logged the inverse. Confirmed. Payload key renamed lowered_monthsneeds_daily_repair, and the worker now emits the backend's own sentence. The test only asserted the month string, which is how the inverted verb survived; it now asserts the direction.

4. backfill_metrics exits 0 with everything broken. Confirmed. _collect_metrics returns its failure count — matching _collect_org_metrics in tasks.py rather than an instance attribute — and the command raises CommandError.

Second tier

Fixed: NULL-org guard (worse than you wrote — unique_monthly_metric includes the nullable org and PG indexes are NULLS DISTINCT, so ON CONFLICT never matches and each run INSERTs another duplicate; pinned by a test that runs the rollup three times). SoftTimeLimitExceeded re-raised ahead of all three broad catches. 0006's interval. The truncation count. The README's SET search_path. The stale docs — you listed four sites, a sweep found six; the module docstring and the runtime warning both gave advice the guard had inverted.

On 0006: the consequence is sharper than a later ValidationError. mirror_pg_periodic_tasks checks crontab first while Beat's PeriodicTask.scheduler checks interval first, so a row carrying both fires on the interval under Beat and mirrors to PG as the crontab — the same row, two cadences. Verified on two fresh databases: interval=NULL with the fix, interval=1 crontab=5 without.

Not fixed, with evidence

  • new_total__lt float comparison. The drift is real in general — I measured ~5e-12 across parallel aggregate runs on PG 15. It cannot fire here: the check is a correlated subquery, and correlated SubPlans are parallel-restricted (measured: no Gather in the plan even with parallel_setup_cost=0), while the write path streams through .iterator(), which issues a server-side cursor that Postgres does not parallelise. Both sides are the serial sum over identical rows.
  • hourly_start untruncated. daily_start did not gain truncate_to_day in this PR — it was already truncated at the merge-base. The PR renamed the helper and parameterised the window. The asymmetry is real and pre-existing.
  • _run records success: False as succeeded. _call_internal raises on non-200 and _run returns 500 on an exception. For a fire-and-forget periodic the PG consumer records nothing either way — no status row, no counter — so raising changes nothing except a poison-drop at MAX_ATTEMPTS=1. The two breakage arms are now logger.error, which is the only signal that reaches an alert.
  • 0006 fall-through rewriting ownership. _inherited_ownership reads from the rows being written, before any write, and neither .update() touches those columns — it writes back what it read.
  • merge_schedules raising on rollback. The forward path repairs the one-transport shape rather than tolerating it, so both transports carry the row afterwards and the guard is unreachable on a shape the forward path produced.

Tests

All four holes closed, and each is mutation-proven with the mutation you named: the pre-existing tests stay green under it while the new ones fail. On the lock — your claim holds one level up, not at the helper: gutting _release_aggregation_locks already killed two tests, but replacing the task's own finally body with pass left the suite fully green. That is what the new test covers.

Deferred

tasks.py at 1130 lines wants the split you describe, but not in this PR — it would bury these fixes under a structural diff in the file you need to re-review. Same for making the backfill's error count per-metric rather than per (org × metric).

Two defects I introduced, and how they were caught

Worth stating plainly, because both were in the fixes for your findings and both were found by review passes rather than by me:

The coverage check hid the single-missing-day case. It counted today in the numerator while measuring elapsed days against yesterday, so today's row cancelled exactly one missing earlier day. Since the daily/monthly row runs at :20, today normally has rows — meaning one missed aggregation, the case the check exists for, was silent, and it only fired at two or more. My own tests passed because the fixture stopped at day 9 while "today" was day 10, so the pair never exercised the production shape.

_run_diagnostic swallowed SoftTimeLimitExceeded — a fourth site of the exact hazard your finding had me fix in three others. Swallowed, the check returns [] and the rollup runs its fleet-wide upsert with the guard off, with under a minute before the hard limit.

Both fixed and mutation-pinned. A later pass also found that my migrate dashboard_metrics 0004 --fake recovery note was a no-op on the image an operator would actually be standing on — I had verified it on the incoming image, which is the one image they are not on. Corrected, and measured both ways.

Deferred, not forgotten

  • tasks.py at 1130 lines wants the split you describe — not in this PR, where it would bury these fixes under a structural diff in the file you need to re-review.
  • The backfill's error count is per (org × metric), so one broken metric on a large fleet reports "500 error(s)". The non-zero exit is right; the count is a poor signal.
  • The coverage warning repeats roughly 25x/day for up to 62 days after a single fleet-idle day. The obvious fix — report only contiguous gaps — would reintroduce the one-missing-day defect above, so this needs a call on warn frequency rather than a patch.

@sonarqubecloud

sonarqubecloud Bot commented Sep 9, 2026

Copy link
Copy Markdown

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
e2e-api-deployment e2e 3 0 0 0 22.6
e2e-coowners e2e 1 0 0 0 1.5
e2e-etl e2e 1 0 0 0 8.2
e2e-login e2e 2 0 0 0 1.2
e2e-prompt-studio e2e 1 0 0 0 9.8
e2e-smoke e2e 2 0 0 0 1.4
e2e-workflow e2e 1 0 0 0 20.2
frontend unit 0 1 0 0 0.0
integration-backend integration 443 0 0 26 45.1
integration-connectors integration 1 0 0 7 8.8
integration-workers integration 157 0 0 1 44.6
ui e2e 0 1 0 0 0.0
unit-backend unit 1217 0 0 1 35.1
unit-connectors unit 63 0 0 0 8.7
unit-core unit 33 0 0 0 1.0
unit-platform-service unit 15 0 0 0 2.4
unit-rig unit 120 0 0 0 3.9
unit-runner unit 5 0 0 0 3.6
unit-sdk1 unit 563 0 0 0 26.4
unit-workers unit 1411 0 0 1 117.8
TOTAL 4039 2 0 36 362.4

Critical paths

⚠️ Critical paths not yet covered

  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (declared coverage: no groups declared)
✅ Covered critical paths
  • auth-login — covered by e2e-login
  • adapter-register-llm — covered by integration-backend
  • workflow-author — covered by integration-backend
  • co-owner-manage — covered by integration-backend, e2e-coowners
  • workflow-create-execute — covered by e2e-workflow
  • api-deployment-provision — covered by integration-backend
  • api-deployment-auth — covered by integration-backend
  • api-deployment-run — covered by e2e-api-deployment
  • mcp-server-auth — covered by integration-backend
  • mcp-platform-auth — covered by integration-backend
  • prompt-studio-author — covered by integration-backend
  • prompt-studio-fetch-response — covered by e2e-prompt-studio
  • connector-register-test — covered by integration-backend
  • pipeline-etl-execute — covered by e2e-etl
  • usage-aggregate-read — covered by integration-backend
  • usage-token-tracking — covered by e2e-api-deployment
  • callback-result-delivery — covered by e2e-api-deployment

@kirtimanmishrazipstack
kirtimanmishrazipstack merged commit df7b74d into main Sep 9, 2026
10 checks passed
@kirtimanmishrazipstack
kirtimanmishrazipstack deleted the UN-3883-Optimize-DB-cron-queries-causing-high-DB-load branch September 9, 2026 12:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants